I’ve named this project echoevm.com. The main goal is to start from the simplest stack operations and gradually build a complete Ethereum bytecode execution environment.
Why choose this direction? Let’s analyze the technical modules of an Ethereum client:
Overall, I lean toward doing something engineering-focused rather than academic, yet still technically challenging. It should be meaningful both for personal technical growth and for the potential outcomes. If this minimal EVM is successfully developed, it can lead to a series of follow-up achievements, and many valuable products can be built on top of it.
The conversion from Solidity to bytecode is compiler expert territory. What I aim to do is execute the bytecode — starting with the simplest operations like addition and jump, then move on to gas calculation, context switching, and ultimately be able to execute all historical Ethereum transactions.
This version adds the ability to run runtime bytecode, meaning you can first deploy a contract and then call the deployed contract’s functions with specific parameters. For example:
go run ./cmd/echoevm -bin ./build/Add.bin -function 'add(uint256,uint256)' -args "3,5"
This command executes the bytecode from the ./build/Add.bin
file and calls the add function, passing in the arguments 3 and 5. After the program completes, it will return the computation result, which is 8.
Implemented a very simple version. Now you can use solc
to compile an Add.sol contract, and then have echoevm
read the generated Add.bin
deployment code, which will output the contract’s runtime code after deployment.
During the implementation of this version, I learned the difference between deployment code and runtime code. Typically, we first deploy a contract to the blockchain and then make calls to it. These are actually two different operations, but both use the same EVM execution. The EVM doesn’t care whether the input bytecode is for deployment or invocation; it simply handles different opcodes differently. Deployment code generally includes both the CODECOPY
and RETURN
opcodes, which can be used to distinguish the type of input.
This version adds the ability to run runtime bytecode—that is, first deploy the contract and then call its functions, optionally passing parameters. For example:
go run ./cmd/echoevm -bin ./build/Add.bin -function 'add(uint256,uint256)' -args "3,5"
This command executes the bytecode in ./build/Add.bin
, calls the add function with arguments 3 and 5, and returns 8 when the program finishes.
Good news—echoevm can now execute contract transactions in the first 10 000 blocks of the Ethereum mainnet! (Because there were no contract transactions in those early blocks :P)
This release introduces block-execution mode: you can run a single block or a range of blocks. You’ll need a URL that serves block data; for early Ethereum blocks, be sure to use an archive-mode node. The full command looks like this:
echoevm -start-block 0 -end-block 10000 -rpc <url>
echoevm’s bytecode support is still limited. Running more recent blocks may raise errors about unsupported opcodes; that’s expected.
This version adds the ability to read bytecode directly from an artifact file—the JSON artifact generated by a Hardhat project when it compiles. Earlier versions could only read the binary file produced by solc
. Previously, you’d compile and run like this:
# Compile the contract to produce bytecode
npx --yes solc --bin Add.sol -o ./build
# Run echoevm to execute the bytecode
go run ./cmd/echoevm run -bin ./test/bins/build/Add_sol_Add.bin -function "add(uint256,uint256)" -args "1,2"
Now it’s simpler. For a standard Hardhat project, every compile creates an artifact file, and echoevm can read the JSON directly:
# Compile the Hardhat project’s contracts
npx hardhat compile
# (Optional) run the project’s tests
npx hardhat test
# Run echoevm to execute the bytecode
go run ./cmd/echoevm run -artifact ./test/contract/artifacts/contracts/Add.sol/Add.json -function "add(uint256,uint256)" -args "1,2"
This release also adds support for more opcodes, but it’s still not enough to execute a full Ethereum block. Next we’ll add test cases incrementally by Solidity feature and examine missing opcodes—hence the focus on execution improvements in this version.
This is a minor release that adds a comprehensive set of Solidity contracts as test cases, covering basic data types, functions, control flow, modifiers, events, interfaces, libraries, inline assembly, and other Solidity features.
A handy command is provided; run it from the project root to see all the test results:
make test-advanced
All tests pass. However, echoevm still can’t execute Ethereum mainnet block 10 000 000, indicating the missing opcodes aren’t part of Solidity’s core syntax—they must come from elsewhere.